package com.cc.service;

import com.cc.monitor.*;
import com.cc.monitor.collect.AbstractCollect;
import com.cc.monitor.collect.JdbcCommonCollect;
import com.cc.monitor.db.DynamicDb;
import com.cc.monitor.entity.job.Job;
import com.cc.monitor.entity.job.Metrics;
import com.cc.monitor.entity.job.protocol.JdbcProtocol;
import com.cc.monitor.entity.message.Code;
import com.cc.monitor.entity.message.Field;
import com.cc.monitor.entity.message.MetricsData;
import com.cc.monitor.entity.message.ValueRow;
import com.cc.monitor.unit.UnitConvert;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlExpression;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import org.noear.solon.data.sql.Row;
import org.noear.solon.data.sql.RowList;
import org.noear.solon.data.sql.SqlUtils;
import org.yaml.snakeyaml.Yaml;

import java.io.IOException;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

/**
 * AppService
 *
 * @author aa
 * @version 1.0
 * @since 2024/11/21
 */
@Slf4j
@Component
public class AppService {

    private final Map<String, Job> jobMap = new HashMap<>(1);

    @Inject
    private WorkerPool workerPool;

    @Inject
    private List<UnitConvert> unitConvertList;

    @Inject
    private DynamicDb dynamicDb;

    @Inject
    private SqlUtils sqlUtils;


    public AppService() {
        initYml();
    }

    public JdbcProtocol addMonitor(JdbcProtocol pt) {
        String sql = "INSERT INTO db_info (host, port, platform, username, password, database,dbName) VALUES (?, ?, ?, ?, ?, ?,?);";
        try {
            Integer id = sqlUtils.sql(sql, pt.getHost(), pt.getPort(), pt.getPlatform(), pt.getUsername(), pt.getPassword(), pt.getDatabase())
                                 .updateReturnKey();
            pt.setId(id.longValue());
            return pt;
        } catch (SQLException e) {
            log.error("数据库监控信息执行SQL错误", e);
        }
        return null;
    }

    public JdbcProtocol getJdbcProtocol(Long monitorId) {
        try {
            Row row = sqlUtils.sql("select * from db_info where id = ?;", monitorId).queryRow();
            if (row != null) {
                return row.toBean(JdbcProtocol.class);
            }
        } catch (SQLException e) {
            log.error("查询数据库监控信息执行SQL错误", e);
        }
        return null;
    }

    public List<JdbcProtocol> getJdbcProtocols() {
        try {
            RowList rows = sqlUtils.sql("select * from db_info;").queryRowList();
            if (rows != null) {
                return rows.toBeanList(JdbcProtocol.class);
            }
        } catch (SQLException e) {
            log.error("查询数据库监控信息执行SQL错误", e);
        }
        return List.of();
    }

    public void deleteJdbcProtocols(Long monitorId) {
        try {
            sqlUtils.sql("delete from db_info where monitor_id = ?;", monitorId).update();
        } catch (SQLException e) {
            log.error("删除数据库监控信息执行SQL错误", e);
        }
    }

    public Map<String, List<SeriesData>> monitor(Long monitorId) {
        final JdbcProtocol jdbcProtocol = getJdbcProtocol(monitorId);
        if (jdbcProtocol == null) {
            throw new RuntimeException("监控信息不存在");
        }
        Job job = jobMap.get("job");
        long jobId = 111L;
        job.setId(jobId);
        job.setMonitorId(monitorId);
        final List<MetricsData> mdList = new ArrayList<>();
        List<Metrics> metricsList = job.getMetrics().stream().peek(metric -> {
            metric.setJdbc(jdbcProtocol);
            metric.setCollectTime(System.currentTimeMillis());
            // Determine whether to configure aliasFields If not, configure the default
            if ((metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) && metric.getFields() != null) {
                metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList()));
            }
            // Set the default metrics execution priority, if not filled, the default last priority
            if (metric.getPriority() == null) {
                metric.setPriority(Byte.MAX_VALUE);
            }
        }).toList();
        dynamicDb.connDb(jdbcProtocol);
        CountDownLatch cdl = new CountDownLatch(metricsList.size());
        metricsList.forEach(metrics -> workerPool.executeJob(() -> runJob(metrics, job.getApp(), job.getTenantId(), job.getMonitorId(), mdList, cdl)));
        try {
            cdl.await();
        } catch (InterruptedException ignore) {
        }
        Map<String, List<MetricsData>> metricsMap = mdList.stream().collect(Collectors.groupingBy(MetricsData::getMetrics));
        return dataConvert(metricsMap);
    }

    private Map<String, List<SeriesData>> dataConvert(Map<String, List<MetricsData>> map) {
        Map<String, List<SeriesData>> seriesMap = new HashMap<>(map.size());
        map.forEach((k, v) -> {
            List<SeriesData> sdList = new LinkedList<>();
            for (MetricsData md : v) {
                for (ValueRow valueRow : md.getValues()) {
                    for (int i = 0; i < md.getFields().size(); i++) {
                        SeriesData seriesData = new SeriesData();
                        final Field field = md.getFields().get(i);
                        final String columnValue = valueRow.getColumn(i);
                        seriesData.setOrigin(columnValue);
                        seriesData.setTimestamp(md.getTime());
                        seriesData.setField(field);
                        sdList.add(seriesData);
                    }
                }
            }
            seriesMap.put(k, sdList);
        });
        return seriesMap;
    }

    public void runJob(Metrics metrics, String app, long tenantId, long monitorId, final List<MetricsData> mdList, CountDownLatch cdl) {
        try {
            long startTime = System.currentTimeMillis();
            MetricsData response = new MetricsData();
            response.setApp(app);
            response.setId(monitorId);
            response.setTenantId(tenantId);
            response.setMetrics(metrics.getName());
            AbstractCollect abstractCollect = new JdbcCommonCollect();
            try {
                abstractCollect.preCheck(metrics);
                abstractCollect.collect(response, monitorId, app, metrics);
            } catch (Exception e) {
                String msg = e.getMessage();
                if (msg == null && e.getCause() != null) {
                    msg = e.getCause().getMessage();
                }
                if (e instanceof IllegalArgumentException) {
                    log.error("[Metrics PreCheck]: {}.", msg, e);
                } else {
                    log.error("[Metrics Collect]: {}.", msg, e);
                }
                response.setCode(Code.FAIL);
                if (msg != null) {
                    response.setMsg(msg);
                }
            }
            calculateFields(metrics, response);
            mdList.add(validateResponse(response, startTime));
        } finally {
            cdl.countDown();
        }

    }

    public void initYml() {
        Yaml yaml = new Yaml();
        try (var inputStream = AppService.class.getClassLoader().getResourceAsStream("app-mysql.yml")) {
            var job = yaml.loadAs(inputStream, Job.class);
            jobMap.put("job", job);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            log.error("Ignore this template file: app-mysql.yml.");
        }
    }


    private MetricsData validateResponse(MetricsData builder, long startTime) {
        long endTime = System.currentTimeMillis();
        builder.setTime(endTime);
        long runningTime = endTime - startTime;
        if (builder.getCode() != Code.SUCCESS) {
            log.info("[Collect Failed, Run {}ms] Reason: {}", runningTime, builder.getMsg());
        } else {
            log.info("[Collect Success, Run {}ms].", runningTime);
        }
        return builder;
    }

    /**
     * Calculate the real metrics value according to the calculates and aliasFields configuration
     *
     * @param metrics     Metrics configuration
     * @param collectData Data collection
     */
    private void calculateFields(Metrics metrics, MetricsData collectData) {
        collectData.setPriority(metrics.getPriority());
        List<Field> fieldList = new LinkedList<>();
        for (Metrics.Field field : metrics.getFields()) {
            Field.FieldBuilder fieldBuilder = Field.builder();
            fieldBuilder.name(field.getField()).type(field.getType()).label(field.isLabel());
            if (field.getUnit() != null) {
                fieldBuilder.unit(field.getUnit());
            }
            fieldList.add(fieldBuilder.build());
        }
        collectData.addAllFields(fieldList);
        List<ValueRow> aliasRowList = collectData.getValues();
        if (aliasRowList == null || aliasRowList.isEmpty()) {
            return;
        }
        // collectData.clearValues();
        // Preprocess calculates first
        if (metrics.getCalculates() == null) {
            metrics.setCalculates(Collections.emptyList());
        }
        // eg: database_pages=Database pages unconventional mapping
        Map<String, String> fieldAliasMap = new HashMap<>(8);
        Map<String, JexlExpression> fieldExpressionMap = metrics.getCalculates().stream().map(cal -> transformCal(cal, fieldAliasMap))
                                                                .filter(Objects::nonNull)
                                                                .collect(Collectors.toMap(arr -> (String) arr[0], arr -> (JexlExpression) arr[1], (oldValue, newValue) -> newValue));

        if (metrics.getUnits() == null) {
            metrics.setUnits(Collections.emptyList());
        }
        Map<String, Pair<String, String>> fieldUnitMap = metrics.getUnits().stream().map(this::transformUnit).filter(Objects::nonNull)
                                                                .collect(Collectors.toMap(arr -> (String) arr[0], arr -> (Pair<String, String>) arr[1], (oldValue, newValue) -> newValue));

        List<Metrics.Field> fields = metrics.getFields();
        List<String> aliasFields = metrics.getAliasFields();
        Map<String, String> aliasFieldValueMap = new HashMap<>(8);
        Map<String, Object> fieldValueMap = new HashMap<>(8);
        Map<String, Object> stringTypefieldValueMap = new HashMap<>(8);
        Map<String, String> aliasFieldUnitMap = new HashMap<>(8);
        List<ValueRow> vrList = new ArrayList<>();
        for (ValueRow aliasRow : aliasRowList) {
            ValueRow realValueRowBuilder = new ValueRow();
            for (int aliasIndex = 0; aliasIndex < aliasFields.size(); aliasIndex++) {
                String aliasFieldValue = aliasRow.getColumn(aliasIndex);
                String aliasField = aliasFields.get(aliasIndex);
                if (!"&nbsp;".equals(aliasFieldValue)) {
                    aliasFieldValueMap.put(aliasField, aliasFieldValue);
                    // whether the alias field is a number
                    CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil.extractDoubleAndUnitFromStr(aliasFieldValue);
                    if (doubleAndUnit != null && doubleAndUnit.getValue() != null) {
                        fieldValueMap.put(aliasField, doubleAndUnit.getValue());
                        if (doubleAndUnit.getUnit() != null) {
                            aliasFieldUnitMap.put(aliasField, doubleAndUnit.getUnit());
                        }
                    } else {
                        fieldValueMap.put(aliasField, aliasFieldValue);
                    }
                    stringTypefieldValueMap.put(aliasField, aliasFieldValue);
                } else {
                    fieldValueMap.put(aliasField, null);
                    stringTypefieldValueMap.put(aliasField, null);
                }
            }
            for (Metrics.Field field : fields) {
                String realField = field.getField();
                JexlExpression expression = fieldExpressionMap.get(realField);
                String value = null;
                String aliasFieldUnit = null;
                if (expression != null) {
                    try {
                        Map<String, Object> context;
                        if (1 == field.getType()) {
                            context = stringTypefieldValueMap;
                        } else {
                            for (Map.Entry<String, String> unitEntry : aliasFieldUnitMap.entrySet()) {
                                if (expression.getSourceText().contains(unitEntry.getKey())) {
                                    aliasFieldUnit = unitEntry.getValue();
                                    break;
                                }
                            }
                            context = fieldValueMap;
                        }

                        // Also executed when valueList is empty, covering pure string assignment expressions
                        Object objValue = JexlExpressionRunner.evaluate(expression, context);

                        if (objValue != null) {
                            value = String.valueOf(objValue);
                        }
                    } catch (Exception e) {
                        log.info("[calculates execute warning] {}.", e.getMessage());
                    }
                } else {
                    // does not exist then map the alias value
                    String aliasField = fieldAliasMap.get(realField);
                    if (aliasField != null) {
                        value = aliasFieldValueMap.get(aliasField);
                    } else {
                        value = aliasFieldValueMap.get(realField);
                    }

                    if (value != null) {
                        final byte fieldType = field.getType();
                        if (fieldType == 0) {
                            CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil.extractDoubleAndUnitFromStr(value);
                            final Double tempValue = doubleAndUnit == null ? null : doubleAndUnit.getValue();
                            value = tempValue == null ? null : String.valueOf(tempValue);
                            aliasFieldUnit = doubleAndUnit == null ? null : doubleAndUnit.getUnit();
                        } else if (fieldType == 3) {
                            final int tempValue;
                            value = (tempValue = CollectUtil.parseTimeStrToSecond(value)) == -1 ? null : String.valueOf(tempValue);
                        }
                    }
                }

                Pair<String, String> unitPair = fieldUnitMap.get(realField);
                if (aliasFieldUnit != null) {
                    if (unitPair != null) {
                        unitPair.setLeft(aliasFieldUnit);
                    } else if (field.getUnit() != null && !aliasFieldUnit.equalsIgnoreCase(field.getUnit())) {
                        unitPair = Pair.of(aliasFieldUnit, field.getUnit());
                    }
                }
                if (value != null && unitPair != null) {
                    for (UnitConvert unitConvert : unitConvertList) {
                        if (unitConvert.checkUnit(unitPair.getLeft()) && unitConvert.checkUnit(unitPair.getRight())) {
                            value = unitConvert.convert(value, unitPair.getLeft(), unitPair.getRight());
                        }
                    }
                }
                // Handle metrics values that may have units such as 34%, 34Mb, and limit values to 4 decimal places
                if (0 == field.getType()) {
                    value = CollectUtil.parseDoubleStr(value, field.getUnit());
                }
                if (value == null) {
                    value = "&nbsp;";
                }
                realValueRowBuilder.addColumns(value);
            }
            aliasFieldValueMap.clear();
            fieldValueMap.clear();
            aliasFieldUnitMap.clear();
            stringTypefieldValueMap.clear();
            vrList.add(realValueRowBuilder);
        }
        collectData.clearValues();
        collectData.getValues().addAll(vrList);
    }


    /**
     * @param cal           cal
     * @param fieldAliasMap field alias map
     * @return expr
     */
    private Object[] transformCal(String cal, Map<String, String> fieldAliasMap) {
        int splitIndex = cal.indexOf("=");
        String field = cal.substring(0, splitIndex).trim();
        String expressionStr = cal.substring(splitIndex + 1).trim().replace("\\#", "#");
        JexlExpression expression;
        try {
            expression = JexlExpressionRunner.compile(expressionStr);
        } catch (Exception e) {
            fieldAliasMap.put(field, expressionStr);
            return null;
        }
        return new Object[]{field, expression};
    }


    /**
     * transform unit
     *
     * @param unit unit
     * @return units
     */
    private Object[] transformUnit(String unit) {
        int equalIndex = unit.indexOf("=");
        int arrowIndex = unit.indexOf("->");
        if (equalIndex < 0 || arrowIndex < 0) {
            return null;
        }
        String field = unit.substring(0, equalIndex).trim();
        String originUnit = unit.substring(equalIndex + 1, arrowIndex).trim();
        String newUnit = unit.substring(arrowIndex + 2).trim();
        return new Object[]{field, Pair.of(originUnit, newUnit)};
    }

}
